<?php

/**
 * Обеспечивает управление объектами и их целостность 
 */
class ObjectsController implements iObjectsController {

	private $laodedObjects = array ();
	private static $instance = false;
	
	/**
	 * Возвращает объект класса ObjectsController
	 * @return ObjectsController
	 */
	public static function getInstance () {
		if (self::$instance === false) {
			self::$instance = new ObjectsController;
		}
		return self::$instance;
	}
	
	private function __construct () {}
	
	public function __clone () {
		throw new CoreException (lang('error_bjects_controller_clone',__CLASS__));
	}
	
	/**
	 * Получить объект
	 * @param Type $type тип объекта
	 * @param integer $id id объекта
	 * @param integer $cacheLifetime время хранения объекта в кеше (в секундах)
	 * @return Object
	 */
	final public function getObject (Type $type, $id, $cacheLifetime = false) {
		$id = (int) $id;
		$key = $this->getObjectKey ($type, $id);
		
		if ($id == 0) {
			return false;
		}
		
		if (isset ($this->laodedObjects[$key])) {
			return $this->laodedObjects[$key];
		}
		
		$cacheController = CacheController::getInstance ();
		
		$cacheParams = array (
							'key' => $key,
							'dir' => 'objects'
						);
						
		if ($cacheLifetime !== false) {
			$cacheParams['lifetime'] = (int) $cacheLifetime;
		}
		
		if ($object = $cacheController->getObject ($cacheParams)) {
			$this->laodedObjects[$key] = $object;
			return $object;
		}
		
		$object = new Object ($type, $id);
		
		$db = DBController::getInstance ();
			
		// Выбираем объект из БД
		$get = $db->query ((string) new ObjectQuery ($type, $id));

		if ($db->num_rows ($get) != 1) {
			return $this->laodedObjects[$key] = false;
		}
			
		// Записываем свойства
		$object->setValues ($db->fetch_array ($get), false);
			
		// Выбираем справочники и теги
		$aFields = $type->getFields ();
		foreach ($aFields as $oField) {
			$character = $oField->getCharacter ();
			if ($character == 'guide' || $character == 'multiguide') {
				$object->setValue ($oField->getName(), $this->getObjectGuideValues ($id, $oField), false);
			}
			elseif ($character == 'tags') {
				$object->setValue ($oField->getName(), $this->getObjectTagsValues ($id, $oField), false);
			}
		}
				
		$this->laodedObjects[$key] = $object;
		
		$cacheParams['value'] = $this->laodedObjects[$key];
		$cacheController->writeObject ($cacheParams);
		
		return $this->laodedObjects[$key];
	}
	
	/**
	 * Получить списк объектов
	 * @param array $aSelected массив со значениями типа и id объектов (такой же возвращает Selection::getList()) 
	 * @param integer $cacheLifetime время хранения объекта в кеше (в секундах)
	 * @return array массив объектов
	 */
	public function getObjectsList (array $aSelected, $cacheLifetime = false) {
		$cacheController = CacheController::getInstance ();
		$typesController = TypesController::getInstance ();
		
		$aResponse = array ();
		$aNeedLoad = array ();
		
		$n = 0;
		
		// Здесь мы пытаемся получить объекты из кеша
		foreach ($aSelected as $row) {
			if (empty ($row[0]) || empty ($row[1])) {
				throw new CoreException ('wrong list element in $aSelected');
			}
			if (!$oType = $typesController->getTypeById ($row[1])) {
				throw new CoreException ('cant found type width id ' . $row[1]);
			}
			
			$key = $this->getObjectKey ($oType, $row[0]);
			
			if (isset ($this->laodedObjects[$key])) {
				$aResponse[$n] = $this->laodedObjects[$key];
				++$n;
				continue;
			}
						
			$cacheParams = array (
								'key' => $key,
								'dir' => 'objects'
							);
							
			if ($cacheLifetime !== false) {
				$cacheParams['lifetime'] = (int) $cacheLifetime;
			}
			
			if ($object = $cacheController->getObject ($cacheParams)) {
				$aResponse[$n] = $object;
			} else { // Объекта в кеше нет - оставляем для него пустое место
				$aResponse[$n] = false;
				$this->laodedObjects[$key] = false;
				
				$aNeedLoad[$row[1]]['ids'][] = $row[0];
				$aNeedLoad[$row[1]]['pos'][$row[0]] = $n;
				
			}
			
			++$n;
		}
				
		// Круто! Все объекты были в кеше и нам не надо их загружать - возвращаем
		if (!count ($aNeedLoad)) {
			return $aResponse;
		}
		
		$db = DBController::getInstance ();
		
		// Теперь загружаем объекты, которых не было в кеше
		foreach ($aNeedLoad as $typeId => $aObjectsInfo) {
			$oType = $typesController->getTypeById ($typeId);

			// Выбираем полея типа справочники и теги, потом пригодятся
			$aGuideFilelds = array ();
			$aTagsFields = array ();
		
			$aFields = $oType->getFields ();
			foreach ($aFields as $oField) {
				$character = $oField->getCharacter ();
				if ($character == 'guide' || $character == 'multiguide') {
					$aGuideFilelds[] = $oField;
				}
				elseif ($character == 'tags') {
					$aTagsFields[] = $oField;
				}
			}
			
			// Отделяем кости от мяса
			$aObjectsIds = $aObjectsInfo['ids'];
			$aObjectsPos = $aObjectsInfo['pos'];
						
			// Выбираем объекты из БД
			$get = $db->query ((string) new ObjectQuery ($oType, $aObjectsIds));
			
			while ($values = $db->fetch_array ($get)) {
				$object = new Object ($oType);
				$objectId = $values['id'];
				$object->setId ($objectId);
				$object->setValues ($values, false);
				
				$pos = $aObjectsPos[$objectId];
				
				$key = $this->getObjectKey ($oType, $objectId);
				$aResponse[$pos] = $object;
				$this->laodedObjects[$key] = $object;
				
				/*$cacheParams = array (
									'key' => $key,
									'dir' => 'objects',
									'value' => $object
								);
								
				if ($cacheLifetime !== false) {
					$cacheParams['lifetime'] = (int) $cacheLifetime;
				}
				
				$cacheController->writeObject ($cacheParams);*/
			}
			
			// Выбираем справочники и теги
			foreach ($aGuideFilelds as $oField) {
				$aGuideValues = $this->getObjectGuideValues ($aObjectsIds, $oField);
				
				foreach ($aGuideValues as $objectId => $values) {
					$key = $this->getObjectKey ($oType, $objectId);					
					
					if ($this->laodedObjects[$key]) {
						$this->laodedObjects[$key]->setValue ($oField->getName(), $values, false);
					}
				}
			}
			
			foreach ($aTagsFields as $oField) {
				$aTagsValues = $this->getObjectTagsValues ($aObjectsIds, $oField);
				
				foreach ($aTagsValues as $objectId => $values) {
					$key = $this->getObjectKey ($oType, $objectId);					
					
					if ($this->laodedObjects[$key]) {
						$this->laodedObjects[$key]->setValue ($oField->getName(), $values, false);
					}
				}
			}
			
			foreach ($aObjectsIds as $objectId) {
				$key = $this->getObjectKey ($oType, $objectId);	
				
				if ($this->laodedObjects[$key]) {
					$cacheParams = array (
										'key' => $key,
										'dir' => 'objects',
										'value' => $this->laodedObjects[$key]
									);
									
					if ($cacheLifetime !== false) {
						$cacheParams['lifetime'] = (int) $cacheLifetime;
					}
					
					$cacheController->writeObject ($cacheParams);
				}
			}
		}
		
		return $aResponse;
	}
	
	/**
	 * Сбросить кеш объекта
	 * @raram Type $oType тип объекта
	 * @param integer $objectId id объекта
	 * @return void
	 */
	final public function clearObjectCache (Type $oType, $objectId) {
		$objectId = (int) $objectId;
		$key = $this->getObjectKey ($oType, $objectId);
		
		if ($objectId < 1) {
			return false;
		}
		
		$cacheParams = array (
						'key' => $key,
						'dir' => 'objects'
					);
					
		CacheController::getInstance()->delete ($cacheParams);
		
		if (isset ($this->loadedObjects[$key])) {
			unset ($this->loadedObjects[$key]);
		}
	}
	
	/**
	 * Сохраняет объект
	 * @param object экземпляр класса ObjectEntity
	 * @return boolean
	 */
	final public function saveObject ($object) {
		if (!($object instanceof ObjectEntity)) {
			throw new CoreException (lang('error_expected_object_entity',__CLASS__));
		}
		
		if (!$object->isUpdated ()) {
			return true;
		}
		
		$oType = $object->getType ();
		
		$this->validateProperties ($object);
		
		$aProperties = $object->getValues ();
		if (count ($aProperties) == 0) {
			return false;
		}
		
		if (!$objectId = $object->getId()) {
			return false;
		}
		
		$aFields = $oType->getFields ();
		$needGuidesUpdate = array ();
		$needTagsUpdate = array ();
		$needImagesUpdate = array ();
		
		$newValues = array ();
		foreach ($aProperties as $fieldName => $value) {
			if (
			       $fieldName == 'id'
			    || !isset ($aFields[$fieldName])
			    || (!$object->isUpdated ($fieldName) && $aFields[$fieldName]->getCharacter () != 'image')
			    || !$aFields[$fieldName]->isReal ()
			) {
				continue;
			}
			
			$character = $aFields[$fieldName]->getCharacter ();
			
			if ($character == 'guide' || $character == 'multiguide') {
				$needGuidesUpdate[] = $fieldName;
				continue;
			}
			
			if ($character == 'image') {
				if (
					   isset ($_FILES['data'])
				    && isset ($_FILES['data']['tmp_name'])
				    && isset ($_FILES['data']['tmp_name'][$objectId])
				    && isset ($_FILES['data']['tmp_name'][$objectId][$fieldName])
				    && mb_strlen ($_FILES['data']['tmp_name'][$objectId][$fieldName])
				) {
					$needImagesUpdate[] = array (
						$fieldName,
						array (
							'name'     => $_FILES['data']['name'][$objectId][$fieldName],
							'tmp_name' => $_FILES['data']['tmp_name'][$objectId][$fieldName],
							'error'    => $_FILES['data']['error'][$objectId][$fieldName],
							'size'     => $_FILES['data']['size'][$objectId][$fieldName],
							'type'     => $_FILES['data']['type'][$objectId][$fieldName]
						),
						$value
					);
				}

				// Нихуясе... надо оставить на память =)
				/*if ($value == '') {
					if (file_exists (DOC_ROOT . $value)) {
						unlink (DOC_ROOT . $value);
					}
					$newValues[] = '`' . $fieldName . '` = \'\'';
					continue;
				}*/
			}
			
			if ($character == 'tags') {
				$needTagsUpdate[] = $fieldName;
				continue;
			}
			
			$newValues[] = '`' . $fieldName . '` = \'' . $value . '\'';
		}
		$newValues = implode (', ', $newValues);
		
		$db = DBController::getInstance ();
		
		$db->startTransaction ();
		
		$db->query ("UPDATE `" . $oType->getTableName () . "` SET " . $newValues . " WHERE `id` = " . $objectId);
		
		// Обновляем справочники
		foreach ($needGuidesUpdate as $fieldName) {
			$this->saveObjectGuideValues ($object, $aFields[$fieldName]);
		}
		
		// Обновляем теги
		foreach ($needTagsUpdate as $fieldName) {
			$this->saveObjectTagsValues ($object, $aFields[$fieldName]);
		}
		
		// Загружаем изображения
		foreach ($needImagesUpdate as $data) {
			$src = FilesController::uploadImage ($data[1], $aFields[$data[0]]);
			$db->query ("UPDATE `" . $oType->getTableName () . "` SET `" . $data[0] . "` = '" . $src . "' WHERE `id` = " . $objectId);
			$object->setValue ($data[0], $src);
			
			if (file_exists ('.' . $data[2])) {
				unlink ('.' . $data[2]);
			}
		}
		
		$db->stopTransaction ();
		
		$this->clearObjectCache ($oType, $objectId);
		
		$object->setUpdated (false);
		
		return true;
	}
	
	/**
	 * Создает новый объект в БД
	 * @param Object экземпляр класса ObjectEntity
	 * @return boolean
	 */
	final public function createNewObject ($object) {
		if (!($object instanceof ObjectEntity)) {
			throw new CoreException (lang('error_expected_object_entity',__CLASS__));
		}
		
		if (!$object->isUpdated ()) {
			return false;
		}
		
		if ($object->getId () !== 0) {
			return false;
		}
		
		$this->validateProperties ($object);
	
		$oType = $object->getType ();
		$aFields = $oType->getFields ();
		
		$writeValues = array ();
		$writeFields = array ();
		
		$needGuidesUpdate = array ();
		$needTagsUpdate = array ();
		$needImagesUpdate = array ();
		
		foreach ($aFields as $fieldName => $oField) {
			if (!$oField->isReal ()) {
				continue;
			}
			
			$character = $aFields[$fieldName]->getCharacter ();
			
			if ($character == 'guide' || $character == 'multiguide') {
				$needGuidesUpdate[] = $fieldName;
				continue;
			}
			
			if ($character == 'image') {
				if (
					   isset ($_FILES['data'])
				    && isset ($_FILES['data']['tmp_name'])
				    && isset ($_FILES['data']['tmp_name']['new'])
				    && isset ($_FILES['data']['tmp_name']['new'][$fieldName])
				    && mb_strlen ($_FILES['data']['tmp_name']['new'][$fieldName])
				) {
					$needImagesUpdate[] = array (
						$fieldName,
						array (
							'name'     => $_FILES['data']['name']['new'][$fieldName],
							'tmp_name' => $_FILES['data']['tmp_name']['new'][$fieldName],
							'error'    => $_FILES['data']['error']['new'][$fieldName],
							'size'     => $_FILES['data']['size']['new'][$fieldName],
							'type'     => $_FILES['data']['type']['new'][$fieldName]
						));
				}
			}
			
			if ($character == 'tags') {
				$needTagsUpdate[] = $fieldName;
				continue;
			}
			
			$value = $object->getValue ($fieldName);
			
			if ($value !== false) {
				$writeValues[] = "'" . $value . "'";
				$writeFields[] = '`' . $fieldName . '`';
			}
		}
		$writeValues = implode (',', $writeValues);
		$writeFields = implode (',', $writeFields);
		
		$db = DBController::getInstance ();
		
		$db->startTransaction ();
		
		$db->query ("INSERT `" . $oType->getTableName() . "` (`id`, " . $writeFields . ") VALUES (0, " . $writeValues . ")", $objectId);
		
		
		$object->setId ($objectId);
		
		// Создаем значения полей-справочников
		foreach ($needGuidesUpdate as $fieldName) {
			$this->saveObjectGuideValues ($object, $aFields[$fieldName]);
		}
		
		// Создаем значения полей-тегов
		foreach ($needTagsUpdate as $fieldName) {
			$this->saveObjectTagsValues ($object, $aFields[$fieldName]);
		}
		
		// Загружаем изображения
		foreach ($needImagesUpdate as $data) {
			$src = FilesController::uploadImage ($data[1], $aFields[$data[0]]);
			$db->query ("UPDATE `" . $oType->getTableName () . "` SET `" . $aFields[$data[0]]->getName() . "` = '" . $src . "' WHERE `id` = " . $objectId);
			$object->setValue ($data[0], $src);
		}
		
		$db->stopTransaction ();
		
		$object->setUpdated (false);
		
		return true;
	}
	
	/**
	 * Удаляет объект
	 * @param Object $object
	 * @return boolean
	 */
	final public function deleteObject (Object $object) {
		if (($objectId = $object->getId ()) <= 0) {
			return false;
		}
		
		$oType = $object->getType ();
		
		$db = DBController::getInstance ();
		
		$db->startTransaction ();
		
		$aFields = $oType->getFields ();
		
		foreach ($aFields as $oField) {
			$character = $oField->getCharacter ();
			
			if ($character == 'guide' || $character == 'multiguide') {
				$db->query ("DELETE FROM `explay_guides_content` WHERE `object_id` = " . $objectId . " AND `field_id` = " . $oField->getId());
			}
			elseif ($character == 'tags') {
				$db->query ("DELETE FROM `explay_tags` WHERE `object_id` = " . $objectId . " AND `field_id` = " . $oField->getId());
			}
			elseif ($character == 'image') {
				if (file_exists (DOC_ROOT . $object->getValue ($oField->getName ()))) {
					unlink (DOC_ROOT . $object->getValue ($oField->getName ()));
				}
			}
		}
		
		$db->query ("DELETE FROM `explay_votes` WHERE `object_id` = " . $objectId . " AND `type_id` = " . $oType->getId());
		$db->query ("DELETE FROM `" . $oType->getTableName () . "` WHERE `id` = " . $objectId);
		
		$this->clearObjectCache ($oType, $objectId);
		
		$db->stopTransaction ();
		
		$object = null;
		
		return true;
	}
	
	/**
	 * Возвращает ключ объекта
	 * @param Type $oType
	 * @param integer $objectId
	 * @return string
	 */
	final public function getObjectKey (Type $oType, $objectId) {
		return $oType->getName() . '_' . $objectId;
	}
	
	/**
	 * Преводит текущие значения полей их к типу
	 * @param Object
	 * @return void
	 */
	private function validateProperties (Object $object) {
		$oType = $object->getType ();
		$typeFields = $oType->getFields ();
		
		foreach ($object->getValues () as $fieldName => $fieldValue) {
			if (!isset ($typeFields[$fieldName]) || !$typeFields[$fieldName]->isReal()) {
				continue;
			}
			
			$valueBefore = $fieldValue;
			
			switch ($typeFields[$fieldName]->getCharacter ()) {
				case 'integer' : {
					$fieldValue = (int) $fieldValue;
					break;
				}
				case 'float' : {
					$fieldValue = (float) $fieldValue;
					break;
				}
				case 'header' : {
					$fieldValue = safeSql (htmlspecialchars ($fieldValue));
					$fieldValue = mb_substr ($fieldValue, 0, 254);
					break;
				}
				case 'text' : {
					$fieldValue = safeSql ($fieldValue);
					break;
				}
				case 'latin' : {
					$fieldValue = safeSql (translit (htmlspecialchars ($fieldValue)));
					break;
				}
				case 'image' : {
					$fieldValue = safeSql (htmlspecialchars ($fieldValue));
					$fieldValue = mb_substr ($fieldValue, 0, 254);
					if (!file_exists (DOC_ROOT . $fieldValue)) {
						$fieldValue = '';
					}
					break;
				}
				case 'email' : {
					if (!eregi("^[a-z0-9\._-]+@[a-z0-9\._-]+\.[a-z]{2,4}\$", $fieldValue)) {
						$fieldValue = '';
					}
					break;
				}
				case 'birthdate' :
				case 'date' : { // UNIX
					$fieldValue = (int) $fieldValue;
					break;
				}
				case 'boolean' : {
					if ($fieldValue) {
						$fieldValue = 1;
					} else {
						$fieldValue = 0;
					}
					break;
				}
				case 'guide' :
				case 'multiguide' : {
					if (!is_array ($fieldValue)) {
						$fieldValue = array ();
						break;
					}
					$cnt = count ($fieldValue);
					
					for ($i = 0; $i < $cnt; ++$i) {
						$fieldValue[$i] = (int) $fieldValue[$i];
					}
					break;
				}
				case 'tags' : {
					if (!is_array ($fieldValue)) {
						$fieldValue = array ();
						break;
					}
					$cnt = count ($fieldValue);
					
					for ($i = 0; $i < $cnt; ++$i) {
						$fieldValue[$i] = safeSql ($fieldValue[$i]);
					}
					break;
				}
				case 'object_value' : {
					$fieldValue = (int) $fieldValue;
				}
				default : {
					continue;
				}
			}
			
			if (is_numeric ($typeFields[$fieldName]->getMaxlength ())) {
				$fieldValue = mb_substr ($fieldValue, 0, $typeFields[$fieldName]->getMaxlength ());
			}

			if ($valueBefore != $fieldValue) {
				$object->setValue ($fieldName, $fieldValue);
			}
		}
	}
	
	/**
	 * Сохраняет/изменяет свойство объекта типа справочник
	 * @param Object $object
	 * @param Field $oField
	 * @return void
	 */
	private function saveObjectGuideValues (Object $object, Field $oField) {
		$aValues = $object->getValue ($oField->getName ());
		
		if (!is_array ($aValues) && !$object->getId ()) {
			return;
		}
		
		$db = DBController::getInstance ();
		
		$aOldValues = $this->getObjectGuideValues ($object->getId (), $oField);
		
		foreach ($aValues as $selectedId) {
			if (!in_array ($selectedId, $aOldValues)) {
				$db->query ("INSERT INTO `explay_guides_content` VALUES (0, " . $oField->getId() . ", " . $object->getId() . ", " . $selectedId . ")");
			}
		}
		
		foreach ($aOldValues as $oldSelected) {
			if (!in_array ($oldSelected, $aValues)) {
				$db->query ("DELETE FROM `explay_guides_content` WHERE `object_id` = " . $object->getId() . " AND `field_id` = " . $oField->getId() . " AND `selected_id` = " . $oldSelected);
			}
		}
	}
	
	/**
	 * Сохраняет/изменяет свойство объекта типа теги
	 * @param Object $object
	 * @param Field $oField
	 * @return void
	 */
	private function saveObjectTagsValues (Object $object, Field $oField) {
		$aValues = $object->getValue ($oField->getName ());
		
		if (!is_array ($aValues) && !$object->getId ()) {
			return;
		}
		
		$db = DBController::getInstance ();
		
		$aOldValues = $this->getObjectTagsValues ($object->getId (), $oField);
		
		
		foreach ($aValues as $tag) {
			if (!in_array ($tag, $aOldValues)) {
				//exit ("tag {$tag} added");
				$db->query ("INSERT INTO `explay_tags` VALUES (0, " . $oField->getId() . ", " . $object->getId() . ", '" . $tag . "')");
			}
		}
		
		foreach ($aOldValues as $oldTag) {
			if (!in_array ($oldTag, $aValues)) {
				$db->query ("DELETE FROM `explay_tags` WHERE `object_id` = " . $object->getId() . " AND `field_id` = " . $oField->getId() . " AND `value` = '" . $oldTag . "'");
			}
		}
	}
	
	/**
	 * Возвращает массив значений свойства объекта типа "справочник"
	 * @param integer $objectId id объекта
	 * @param Field $oField объект поля
	 * @return array
	 */
	private function getObjectGuideValues ($objectId, Field $oField) {
		$db = DBController::getInstance ();
		
		if (is_array ($objectId)) {
			$get = $db->query ("SELECT `object_id`, `selected_id` FROM `explay_guides_content` WHERE `object_id` IN ('" . implode("','", $objectId) . "') AND `field_id` = " . $oField->getId());
			
			$res = array ();
			while (list ($id, $value) = $db->fetch_row ($get)) {
				$res[$id][] = $value;
			}
			return $res;
		} else {
			$get = $db->query ("SELECT `selected_id` FROM `explay_guides_content` WHERE `object_id` = " . $objectId . " AND `field_id` = " . $oField->getId());
			
			$res = array ();
			while (list ($value) = $db->fetch_row ($get)) {
				$res[] = $value;
			}
			return $res;
		}
	}
	
	/**
	 * Возвращает массив значений свойства объекта типа "теги"
	 * @param integer $objectId id объекта
	 * @param Field $oField объект поля
	 * @return array
	 */
	private function getObjectTagsValues ($objectId, Field $oField) {
		$db = DBController::getInstance ();
		
		if (is_array ($objectId)) {
			$get = $db->query ("SELECT `object_id`, `value` FROM `explay_tags` WHERE `object_id` IN ('" . implode("','", $objectId) . "') AND `field_id` = " . $oField->getId() . " ORDER BY `value`");
	
			$res = array ();
			while (list ($objectId, $tag) = $db->fetch_row ($get)) {
				$res[$objectId][] = $tag;
			}
			return $res;
		} else {
			$get = $db->query ("SELECT `value` FROM `explay_tags` WHERE `object_id` = " . $objectId . " AND `field_id` = " . $oField->getId() . " ORDER BY `value`");
	
			$res = array ();
			while (list ($tag) = $db->fetch_row ($get)) {
				$res[] = $tag;
			}
			return $res;
		}
	}
	
}